{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#  Copyright (c) 2021 Arm Limited. All rights reserved.\n",
    "#  SPDX-License-Identifier: Apache-2.0\n",
    "#\n",
    "#  Licensed under the Apache License, Version 2.0 (the \"License\");\n",
    "#  you may not use this file except in compliance with the License.\n",
    "#  You may obtain a copy of the License at\n",
    "#\n",
    "#      http://www.apache.org/licenses/LICENSE-2.0\n",
    "#\n",
    "#  Unless required by applicable law or agreed to in writing, software\n",
    "#  distributed under the License is distributed on an \"AS IS\" BASIS,\n",
    "#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
    "#  See the License for the specific language governing permissions and\n",
    "#  limitations under the License."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Overview"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Currently if you wish to deploy your machine learning model on Arm Ethos-U55 or Arm Ethos-U65 you need to use TensorFlow Lite for Microcontrollers. Doing so requires you have your model in TensorFlow Lite format first. If you have trained your model using TensorFlow then the process to convert to TensorFlow Lite is already well documented.  \n",
    "\n",
    "However, it might be the case that you have trained your model using [PyTorch](https://pytorch.org/) instead, which does not natively allow you to export to TensorFlow Lite format. This guide will show how it is possible to convert your trained PyTorch model to TensorFlow Lite using ONNX, and then quantize it ready for deploying on Arm Ethos-U55 or Arm Ethos-U65.\n",
    "\n",
    "**Note: Before continuing, please be aware that although this guide shows how to convert a PyTorch trained model to TensorFlow Lite, we recommend you use TensorFlow for model training if possible. This will avoid any translation errors that can occur when converting from PyTorch and you should also see better performance and layer support.**\n",
    "\n",
    "This guide we will start by training a small convolutional neural network in PyTorch on the CIFAR10 dataset, then perform optimizations on the graph before exporting it to ONNX format. Next, the ONNX format model will be converted to TensorFlow saved model format, before finally being loaded into TensorFlow for quantization and conversion to TensorFlow Lite. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"resources/pytorch_arm_ethos_flow.png\">\n",
    "<h6 align=\"center\">Figure 1. Flow of operations to get your model from PyTorch and ready for Arm Ethos-U55 or Arm Ethos-U65.</h6>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Before you begin"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Running the following commands will create a Python environment and install the libraries necessary to run the code sample:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip3 install --upgrade pip\n",
    "!pip3 install torch==1.8.1+cpu torchvision==0.9.1+cpu torchaudio==0.8.1 -f https://download.pytorch.org/whl/torch_stable.html \n",
    "!pip3 install onnx==1.9.0 \n",
    "!pip3 install tensorflow==2.4.1 \n",
    "!git clone https://github.com/onnx/onnx-tensorflow.git \n",
    "!pip3 install ./onnx-tensorflow \n",
    "!pip3 install ethos-u-vela==3.0.0"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torchvision.datasets as datasets\n",
    "import torch.optim as optim\n",
    "from torchvision import transforms\n",
    "from onnx_tf.backend import prepare\n",
    "import onnx\n",
    "import tensorflow as tf\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## From PyTorch to TensorFlow Lite"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "1) Use the following code to define a simple convolutional neural network in PyTorch, that is going to train on the CIFAR10 dataset: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# A small convolutional network to test PyTorch to TFLite conversion. \n",
    "class SimpleNetwork(nn.Module): \n",
    " \n",
    "    def __init__(self):\n",
    "        super(SimpleNetwork, self).__init__()\n",
    "\n",
    "        self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=(3, 3))\n",
    "        self.bn1 = nn.BatchNorm2d(16)\n",
    "        self.conv2 = nn.Conv2d(in_channels=16, out_channels=16, kernel_size=(3, 3))\n",
    "        self.bn2 = nn.BatchNorm2d(16)\n",
    "        self.conv3 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=(3, 3))\n",
    "        self.bn3 = nn.BatchNorm2d(32)\n",
    "        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)\n",
    "        # Test padding conversion. \n",
    "        self.conv4 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3, 3), padding=(1, 1))\n",
    "        self.bn4 = nn.BatchNorm2d(64)\n",
    "        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)\n",
    "\n",
    "        # An affine operation: y = Wx + b \n",
    "        self.conv5 = nn.Conv2d(in_channels=64, out_channels=10, kernel_size=(6, 6))  # Feature size is 6*6 here.\n",
    "        self.softmax = nn.Softmax()\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = F.relu(self.bn1(self.conv1(x)))\n",
    "        x = F.relu(self.bn2(self.conv2(x)))\n",
    "        x = self.pool1(x)\n",
    "        x = F.relu(self.bn3(self.conv3(x)))\n",
    "        x = F.relu(self.bn4(self.conv4(x)))\n",
    "        x = self.pool2(x)\n",
    "        x = self.conv5(x)\n",
    "        x = x.view(-1, 10)\n",
    "        x = self.softmax(x)\n",
    "\n",
    "        return x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this example the model is trained on the CIFAR10 dataset so this dataset is loaded and a basic training loop is then used to train the model. The code below shows how this is implemented:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Load our training and test data and create dataloaders.\n",
    "transform = transforms.Compose([transforms.ToTensor()])\n",
    "cifar_train = datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=transform)\n",
    "cifar_test = datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=transform)\n",
    "\n",
    "train_loader = torch.utils.data.DataLoader(cifar_train, batch_size=200)\n",
    "test_loader = torch.utils.data.DataLoader(cifar_test)\n",
    "\n",
    "# Create an instance of our model.\n",
    "model = SimpleNetwork()\n",
    "\n",
    "# Define the loss function and optimizer we will use to train.\n",
    "loss_func = nn.NLLLoss()\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.001)\n",
    "\n",
    "# Training loop - train for more epochs to get better accuracy.\n",
    "for epoch in range(2):\n",
    "    for i, train_data in enumerate(train_loader, 0):\n",
    "        train_x, train_y = train_data\n",
    "\n",
    "        optimizer.zero_grad()\n",
    "\n",
    "        outputs = model(train_x)\n",
    "\n",
    "        loss = loss_func(torch.log(outputs + 1e-20), train_y)\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "\n",
    "        if i % 20 == 0:\n",
    "            print(f\"Epoch {epoch}, iter {i}, Loss: {loss.item()}\")\n",
    "\n",
    "print(\"Training finished...\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "2) After the model is trained, put it into evaluation mode, so it is ready for exporting using the following code: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.eval()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We also test the accuracy of the model at this point so we can compare later after conversion. With only two epochs of training, we obtain around 64% accuracy - much better than random chance as we can see in the following output:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Test PyTorch model accuracy.\n",
    "num_correct = 0\n",
    "with torch.no_grad():\n",
    "    for data in test_loader:\n",
    "        image, labels = data\n",
    "\n",
    "        outputs = model(image)\n",
    "\n",
    "        _, predicted = torch.max(outputs.data, 1)\n",
    "\n",
    "        num_correct += (predicted == labels).sum().item()\n",
    "\n",
    "print(f\"Accuracy of PyTorch model on test set: {100*num_correct / len(cifar_test)}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3) As we used Batch Normalization layers in our model, one optimization we can do is to fold or fuse these layers into the preceding convolution operation. Folding of fusing can be done by calling torch.quantization.fuse_modules on a list of layer names in the model that must be fused together, like in the following code: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.quantization.fuse_modules(model, [['conv1', 'bn1'], ['conv2', 'bn2'], ['conv3', 'bn3'], ['conv4', 'bn4']], inplace=True) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The Batch Normalization layers are now fused into their preceding convolution layer. Doing this helps to reduce the amount of unnecessary computation carried out by the network, reduces the number of weights, and helps make conversion smoother. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "4) Next, we export our trained PyTorch model to PyTorch and ONNX format. ONNX is a model exchange format focused on inferencing, that acts as an intermediate format between different neural network frameworks. For more information on [ONNX](https://onnx.ai/), PyTorch natively supports exporting to ONNX format. We can export our model to ONNX by calling the following function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Save the trained PyTorch model and export to ONNX.\n",
    "torch.save(model, 'model.pth')\n",
    "torch.onnx.export(model, iter(test_loader).next()[0], \"model.onnx\", input_names=['input'], output_names=['output']) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To export, we need to provide some sample input data so that PyTorch can identify exactly what parts of the graph need to be exported. The export function also allows us to change the names of input and output nodes in the model if we like."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "5) After exporting to ONNX format we can inspect the model file with [Netron](https://lutzroeder.github.io/netron/) and see that it is virtually identical to the original PyTorch model. Note that the identity operations in the PyTorch model are left over from fusing Batch Normalization layers. In the ONNX model the input and output nodes now have the new names we assigned when exporting. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"resources/pytorch_model.png\" align=\"left\"><img src=\"resources/onnx_model.png\" align=\"center\">\n",
    "\n",
    "<h6 align=\"center\">Figure 2. PyTorch model on the left and its exported ONNX version on the right.</h6>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once the model is in ONNX format we can make use of ONNX, and the available ONNX converters, to load and convert the model to TensorFlow format."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "6) Load the ONNX model, prepare it to be converted to TensorFlow and then save to it file in the TensorFlow saved model format using the following code: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "onnx_model = onnx.load(\"model.onnx\")\n",
    "tf_rep = prepare(onnx_model, device='cpu') \n",
    "tf_rep.export_graph(\"model_tf\") "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We choose to set the device to 'cpu' to force operations to be in NHWC format which is required by TensorFlow Lite."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "7) Load our model into TensorFlow using the TFLite converter now that the model is in TensorFlow Save model format, by using the following code: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "converter = tf.lite.TFLiteConverter.from_saved_model(\"model_tf\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Note: The Arm Ethos-U55 is designed to accelerate neural network inferences and supports only 8-bit weights with 8-bit or 16-bit activations. Therefore, to take advantage of the Arm Ethos-U55, your model must be quantized from 32-bit floating point to 8-bit fixed point format.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "8) After loading the converted model with the TFLiteConverter, we perform post-training quantization. The result of this process will be a model with both weights and activations fully quantized so that it can be deployed on Arm Ethos-U55. The following code shows how to perform this quantization:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def rep_dataset():\n",
    "    \"\"\"Generator function to produce representative dataset for post-training quantization.\"\"\"\n",
    "\n",
    "    # Use a few samples from the training set.\n",
    "    for _ in range(100):\n",
    "        img = iter(train_loader).next()[0].numpy()\n",
    "        img = [(img.astype(np.float32))]\n",
    "        yield img"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set options for full integer post-training quantization.\n",
    "converter.optimizations = [tf.lite.Optimize.DEFAULT]\n",
    "converter.representative_dataset = rep_dataset\n",
    "converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]\n",
    "converter.inference_input_type = tf.int8\n",
    "converter.inference_output_type = tf.int8\n",
    "\n",
    "# Convert to TFLite.\n",
    "tflite_model = converter.convert()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "9) Save the quantized model to file, using the following code: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "open(\"model.tflite\", \"wb\").write(tflite_model)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "10) Test the model's accuracy again because it can change slightly after quantization. We can see that in this case quantizing our model has done little to change the accuracy of our model. This test also helps confirm to us that the conversion process has not affected the quality of the model either. The following shows example output from the test: "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## NHWC vs NCHW"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we inspect the resulting TensorFlow Lite file with Netron we can see that a Transpose operator has been added at the start of our graph. PyTorch uses a channel first (NCHW) data layout for its operations, while TensorFlow and TensorFlow Lite primarily use channel last (NHWC). The converter maintains the same input shape as the original PyTorch model so that the same input data can be re-used in the converted model without alteration. As a result, the converter adds a Transpose or Reshape operation so that the converted convolution operations can correctly work with this data."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"resources/transpose.png\" align=top><img src=\"resources/reshape.png\" align=bottom>\n",
    "\n",
    "<h6 align=\"bottom\">Figure 3. Transpose operation added when input channels is greater than one and a reshape operation added otherwise.</h6>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Depending on the shape of the model outputs, the converter can also add a Transpose operation at the end of the model. This operation returns the output shape to the original PyTorch model output shape.\n",
    "\n",
    "Also depending on your model, you may notice the converter adds Transpose operations before and after certain layers in the TensorFlow Lite model. These additions are also a result of the conversion process - and because certain operators are still expecting NCHW inputs even after conversion.\n",
    "\n",
    "For example, these additions can happen when using some activation functions such as ReLU6. The resulting TensorFlow Lite graph looks like the following:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"resources/relu6.png\">\n",
    "<h6 align=\"center\">Figure 4. ReLU6 operation wrapped in Transpose operators after conversion to TensorFlow Lite.</h6>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One notable addition to the TensorFlow Lite graph is the addition of padding operations before convolution layers. TensorFlow has the concept of 'VALID' and 'SAME' padding while PyTorch only allows for explicating padding to be used. Currently the conversion process maintains this explicit padding when converting to TensorFlow, even if a 'SAME' padding in a convolution layer might be equivalent."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"resources/pad.png\">\n",
    "<h6 align=\"center\">Figure 5. Explicit padding operation added in before a convolution layer after conversion to TensorFlow Lite.</h6>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Luckily, as we see later these added pad operations can be fused again if we decide to deploy the model on Arm Ethos-U55 and run it through Vela."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Note: Depending on the version of TensorFlow used when converting to TensorFlow Lite, you can see that the converter adds Transpose operations around these Pad operations as well. In this case no optimization can occur.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Further limitations"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Due to the current limitations of the conversion process, it is likely that the converted TFLite model will be less optimal, with regards to inference speed, than a model trained natively in TensorFlow and then converted to TensorFlow Lite. The main cause for this is the additional Transpose operations that the converter can add to the graph when converting between ONNX and TensorFlow."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can see this more concretely by benchmarking the model converted from PyTorch against the same model architecture trained natively in TensorFlow. One way to currently do this is to take an [MPS3 FPGA board](https://developer.arm.com/tools-and-software/development-boards/fpga-prototyping-boards/mps3) loaded with an Arm Ethos-U55 and Arm Cortex-M55, then profile inference speeds using the [Arm ML Embedded Evaluation Kit's](https://review.mlplatform.org/plugins/gitiles/ml/ethos-u/ml-embedded-evaluation-kit/+/refs/heads/master) Generic Inference Runner application."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can see from the following table that the model converted from PyTorch has an order of magnitude higher CPU cycle count than the native TensorFlow one while they both have similar NPU cycle counts. The added Transpose operator would account for this increase in CPU cycle counts. This extra CPU operation shows up in the additional wall clock inference times for the converted model."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Metric | PyTorch converted model | TF trained model |\n",
    "| --- | --- | --- |\n",
    "| NPU Active Cycles | 98798 | 100979 |\n",
    "| CPU Active Cycles | 489059 | 12220 |\n",
    "\n",
    "<h6 align=\"center\">Table 1. Arm Ethos-U55 cycle counts on FPGA for PyTorch converted model and native TensorFlow trained model.</h6>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As the model used in this guide is a very small one, the time spent on this Transpose operation takes up a considerable proportion of the total wall clock time. For larger and deeper models, we would hope to see the proportion of time spent on this operation to vastly decrease."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One other limitation is that the ONNX to TensorFlow converter does not focus on making your model fully compatible with TensorFlow Lite. As a result, you may come across occasions that a model can convert to TensorFlow but fails to convert to TensorFlow Lite."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Overcoming limitations"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Unfortunately, there seems to be no easy solution to many of these limitations in the conversion process and they are an unavoidable fact of trying to convert a PyTorch model to TensorFlow Lite. The best advice to overcome these issues is to train your model natively in TensorFlow and convert to TensorFlow Lite from there."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Vela optimization "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we have our quantized TensorFlow Lite file we can run it through the Arm Vela compiler to optimize the model for deployment on Arm Ethos-U55.\n",
    "\n",
    "This optimization can be done from the command line by running the following command: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!vela model.tflite"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Default parameters with regards to optimization, and which Arm Ethos-U55 or Arm Ethos-U65 is being targeted, are used but this is okay just to see the resulting model after Vela has optimized it. See https://pypi.org/project/ethos-u-vela/ for more information on the different command line options that you can use with Vela"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"resources/vela.png\">\n",
    "<h6 align=\"center\">Figure 6. The converted TensorFlow Lite model after being run through the Vela compiler.</h6>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the output folder that is created we find the Vela optimised model in TensorFlow Lite format. Inspecting the optimised model, we can see that every node, except for the Transpose layer, has been compiled into one ethos-u operator. This means that all those layers can be successfully run and accelerated on Arm Ethos-U55 or Arm Ethos-U65 NPU. The Transpose layer is instead run on the accompanying Arm Cortex-M CPU."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conclusion "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By following this guide, you should have successfully trained and converted a PyTorch model to TensorFlow format. The model was then quantized in TensorFlow and converted to TensorFlow Lite format, before finally being run through Vela ready to deploy on Arm Ethos-U55 or Arm Ethos-U65."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We have also discussed some of the limitations of this conversion process, mainly the additional Transpose operations introduced, and how this can affect final performance when using Arm Ethos-U55 or Arm Ethos-U65."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ultimately if you have the choice, we highly recommend you train your model natively using the TensorFlow framework and convert to TensorFlow Lite from there. This should result in the most optimal model when you deploy it and should present you with the least problems along the way. However, if that is not possible then as we have shown in this guide conversion from PyTorch may be possible to do and produce a usable model."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
